tinydiffeq 2.3.0: port to nlls-gram 2.7.0 - #8
Merged
Conversation
…s-through nlls-gram 2.6/2.7 collapsed its flat constructor kwargs into solver and preconditioner objects: of the 26 kwargs LMRootSolver mirrored and forwarded, 17 no longer exist. Rather than re-mirror the new surface, LMRootSolver now keeps only the five fields this package actually owns -- max_steps, max_steps_is_success, atol, gtol, xtol, all of which reach the nlls solve rather than its constructor -- and runs everything algorithmic at nlls-gram's defaults. solver_options replaces the 21 dropped knobs with one pass-through forwarded verbatim to the LevenbergMarquardt constructor, so the names track nlls-gram across versions instead of being duplicated here. It normalizes to a sorted tuple: _cached_algebraic_solver is functools.cache'd on the config and falls back to rebuilding on TypeError, and nlls-gram keys its compiled loop on solver identity, so an unhashable config would silently retrace every step. cache_jacobian and geodesic_acceleration stay fixed to False and are rejected by name -- each DAE stage changes the root problem -- rather than being silently honored. Verified against the nlls-gram 2.4.0 baseline on this machine: 251 passed / 11 skipped (the extra test is the new normalization case) against 250 / 11, same 129s. Accuracy is unaffected by ad_solver "auto" -> None, including the 1e-12 implicit-tangent tests. Benchmarks (115 cases) are flat in aggregate, median -0.04%, but that hides a real split on the nlls path, reproducible across two runs: reverse mode is slower (vjp-vector16-dae +18/+22%, vjp-tree16-dae +17/+18%, vjp-scalar-dae +9/+11%, vjp-vector16-sdae +9/+11%) and forward mode faster (jvp-*-dae/sdae -8 to -17%). Every rodas5p_dae case is flat, which fits: Rodas5P uses the root solver only for initial consistency. Probing ad_solver rules out the config as the cause -- default (636us vjp) already beats Cholesky (613us, within noise), SVD (874us) and QR (680us) -- so this is 2.7.0's implicit-AD path, reported rather than worked around. Co-Authored-By: Mecha Perla (Claude) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014og23CSBQfdHGNCfA21F8x
The GPU tests build state with a bare `jnp.asarray(1.0)`, which under the x64 conftest is float64 -- so the core ODE, SDE and DAE GPU paths had no single-precision coverage at all. Four float32-end-to-end tests close that, at a deliberately coarse bar: no crash, on device, finite, and correct to a few digits. Tight float32 agreement is a CPU concern; the point here is that the GPU kernels take the same paths and do not return nonsense. One of them pins the dtype rule in dae.py, which reads z_dtype ALONE to pick root_atol (1e-10 above 32 bits, else 1e-6): a float32 y with a float64 z gets the tight bar, and both dtypes have to survive the round trip on device. conftest also sets jax_default_matmul_precision=highest. XLA:GPU serves float32 dot_general from TF32 tensor cores by default at a 10-bit mantissa, and the float32 tests here check against closed forms with tolerances derived from float32 eps -- the linear-exponential and Markov paths are matmul-heavy enough that TF32 would fail them on GPU while passing on CPU. nlls-gram pins the products it owns, but it cannot reach the matmuls in a caller's vector field, which is exactly what these tests exercise. A no-op on CPU. Co-Authored-By: Mecha Perla (Claude) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014og23CSBQfdHGNCfA21F8x
The two new float32 GPU tests read .xs[-1]. SaveAt defaults to t_1=True, so xs is already the endpoint -- a scalar here, and IndexError on a GPU box. The DAE cases passed only because their state is a 1-element array. Caught by the 3090 run, which is the point of having them. Co-Authored-By: Mecha Perla (Claude) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014og23CSBQfdHGNCfA21F8x
Floors nlls-gram at the released 2.7.0 and drops the local editable source override, so the published wheel resolves from PyPI instead of carrying a path into a checkout. 2.2.0 and earlier are not compatible with nlls-gram 2.7.0: LMRootSolver forwarded 26 constructor kwargs and 17 of them no longer exist, so a fresh install of 2.2.0 against 2.7.0 fails at solver construction. 2.6.0 still accepted them, which is why the break starts now. LMRootSolver went from 26 fields to 6 in that port: the five this package actually owns, which reach the nlls solve rather than its constructor, plus a solver_options pass-through for the rare root that needs a non-default algorithm. Verified against the published wheel: 251 passed, 15 skipped. Co-Authored-By: Mecha Perla (Claude) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014og23CSBQfdHGNCfA21F8x
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ports to nlls-gram 2.7.0 and releases 2.3.0.
Why this is required
nlls-gram 2.6/2.7 collapsed its flat constructor kwargs into typed config
objects. Of the 26 kwargs
LMRootSolvermirrored and forwarded, 17 nolonger exist — so published tinydiffeq 2.2.0 breaks at solver construction
against nlls-gram 2.7.0 (2.6.0 still accepted them).
LMRootSolver: 26 fields to 6
Keeps only the five this package owns, all of which reach the nlls
solverather than its constructor:
max_steps,max_steps_is_success,atol,gtol,xtol. The 21 mirrored algorithm knobs are replaced by onesolver_optionspass-through forwarded verbatim toLevenbergMarquardt, sothe names track nlls-gram across versions instead of being duplicated here.
It normalizes to a sorted tuple:
_cached_algebraic_solverisfunctools.cached on the config and falls back to rebuilding onTypeError,and nlls-gram keys its compiled loop on solver identity — an unhashable config
would silently retrace every step.
cache_jacobianandgeodesic_accelerationstay pinned toFalseand are rejected by name ratherthan silently honored, since each DAE stage changes the root problem.
float32 and GPU
tests/conftest.pypinsjax_default_matmul_precision=highest: XLA:GPUserves float32
dot_generalfrom TF32 tensor cores at a 10-bit mantissa, andthe matmul-heavy exponential and Markov paths are checked against closed forms
at float32-eps tolerances.
Four float32 GPU tests added. The existing ones build state with a bare
jnp.asarray(1.0), which under the x64 conftest is float64 — so the core ODE,SDE and DAE GPU paths had no single-precision coverage at all. One pins the
z_dtype-onlyroot_atolrule.Verification
ad_solverdefault move, including the 1e-12implicit-tangent cases
upstream is closed (
vjp-vector16-dae+18.4% to -1.6%)